﻿/*!
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Threading.Tasks;
using System.Web;

namespace Pittypat.Web
{
    /// <summary>
    /// 向客户端异步发送的数据流。
    /// </summary>
    /// <remarks>
    /// 异步发送的数据流不会在创建 <see cref="AsyncStream"/> 对象实例时就创建要发送的数据，而是在向客户端响应时才会从构造函数的 readAsync 方法中读取要发送的数据。
    /// 可用于在功能的 Api 函数中作为返回值向客户端发送二进制流数据。
    /// </remarks>
    public sealed class AsyncStream : IJson
    {
        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(AsyncStream));

        // 读取二进制数据的方法
        private Func<Task<ArraySegment<byte>?>> readAsync;
        private string fileName;
        private string contentType;

        /// <summary>
        /// 使用异步获取数据的方法、文件名称和文件类型初始化 <see cref="AsyncStream"/> 的新实例。
        /// </summary>
        /// <param name="readAsync">异步读取要发送的数据的方法。</param>
        /// <param name="fileName">重命名的文件名称，如果包含扩展名称，则使用此扩展名称推测文件类型。</param>
        /// <param name="contentType">文件的 MIME 类型，如果没有提供，则根据文件的扩展名称自动推测。</param>
        /// <exception cref="ArgumentNullException">readAsync 为 null。</exception>
        public AsyncStream(Func<Task<ArraySegment<byte>?>> readAsync, string fileName = null, string contentType = null)
        {
            if (readAsync == null)
            {
                throw new ArgumentNullException("readAsync");
            }

            this.fileName = fileName ?? "";
            this.contentType = string.IsNullOrEmpty(contentType) ? Mime.Of(Path.GetExtension(fileName)) : contentType;
            this.readAsync = readAsync;
        }

        /// <summary>
        /// 获取文件名称。
        /// </summary>
        public string FileName
        {
            get
            {
                return this.fileName;
            }
        }

        /// <summary>
        /// 获取文件的类型。
        /// </summary>
        public string ContentType
        {
            get
            {
                return this.contentType;
            }
        }

        /// <summary>
        /// 将当前对象作为 JSON 字符串写入指定的文本流。
        /// </summary>
        /// <param name="writer">目标文本流。</param>
        void IJson.Write(TextWriter writer)
        {
            writer.Write("{{\"fileName\":\"{0}\",\"contentType\":\"{1}\"}}",
                Json.Escape(this.fileName), Json.Escape(this.contentType));
        }

        /// <summary>
        /// 返回文件名称的字符串。
        /// </summary>
        /// <returns>文件名称的字符串。</returns>
        public override string ToString()
        {
            return this.fileName;
        }

        /// <summary>
        /// 通过 WebSocket 发送文件。
        /// </summary>
        /// <param name="progress">汇报异步操作的进度。</param>
        /// <param name="bufferSize">发送缓冲区的尺寸，此参数目前没有使用。</param>
        /// <returns>总共发送的字节数。</returns>
        internal async Task<long> SendAsync(WebSocketAsyncProgress progress, int bufferSize = 0)
        {
            // 已经发送的字节数
            long sent = 0L;
            
            ArraySegment<byte>? data = null;
            do
            {
                // 读取要发送的数据
                data = await this.readAsync();

                if (!data.HasValue)
                {
                    // 没有更多数据
                    break;
                }

                // 增加计数
                sent += data.Value.Count;
                if (data.Value.Count > 0)
                {
                    // 发送数据
                    await progress.Socket.SendAsync(data.Value, WebSocketMessageType.Binary, true, progress.Token);
                }
            } while (true);

            // 总共发送的字节数
            return sent;
        }

        /// <summary>
        /// 将文件作为响应内容附加到指定的响应消息
        /// </summary>
        /// <param name="response">目标响应对象。</param>
        internal void ToResponseContent(HttpResponseMessage response)
        {
            // 响应内容
            response.Content = new PushStreamContent(async (stream, httpContent, context) =>
            {
                try
                {
                    ArraySegment<byte>? data = null;
                    do
                    {
                        data = await this.readAsync();

                        if (!data.HasValue)
                        {
                            // 没有更多数据
                            break;
                        }

                        if (data.Value.Count > 0)
                        {
                            await stream.WriteAsync(data.Value.Array, data.Value.Offset, data.Value.Count);
                        }
                    } while (true);
                }
                catch (HttpException exc)
                {
                    // 读取数据或发送数据过程中发生错误
                    Log.Fatal(exc.Message, exc);
                }
            });

            // 响应内容类型
            response.Content.Headers.ContentType = new MediaTypeHeaderValue(this.contentType);

            // 文件名称
            if (!string.IsNullOrEmpty(fileName))
            {
                response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
                {
                    FileName = this.fileName
                };
            }
        }
    }
}
